前面我們已經知道,當 Signal 的值發生變化後,Angular 會透過 Reactive Graph 找到受到影響的 Consumer。
不過找到 Consumer 只是第一步。對 Template 來說,Angular 還需要知道這個 Consumer 對應到哪一個 View,並在 View Tree 中保留一條可以走到它的路徑。
這樣下一次執行 Change Detection 時,Angular 才能沿著這條路找到真正需要重新檢查的 View,而不是把整個分支直接略過。

Reactive Graph 主要負責記錄 Signal 和 Consumer 之間的依賴關係。
但當 Angular 真正開始執行 Change Detection 時,會沿著 View Tree 往下檢查各個 View。
可以先把 View Tree 想成和「元件樹」很接近的概念,例如:
AppComponent
└── LayoutComponent
└── ProductComponent
補充:不過兩者並不完全一樣,因為
View Tree除了元件之外,也可能包含實際建立出來的 Embedded View,例如@if、@for或ng-template產生的 View。
假設真正受到 Signal 變化影響的是最下面的 ProductComponent,Angular 執行 Change Detection 時,還是需要從上層沿著 View Tree 往下走,才能到達這個 View。
所以即使 Angular 已經透過 Reactive Graph 找到受到影響的 Consumer,還是需要在 View Tree 上留下對應的路徑,避免還沒走到目標 View,就先把這個分支略過。
Reactive Graph 跟 View Tree 之間的關係大致如下:

當 Consumer 被標記為 dirty 時,Angular 除了會先設定 dirty = true,最後還會執行這個 Consumer 自己的 consumerMarkedDirty():
export function consumerMarkDirty(node: ReactiveNode): void {
node.dirty = true;
producerNotifyConsumers(node);
node.consumerMarkedDirty?.(node);
}
對 Template 對應的 ReactiveLViewConsumer 來說,接下來就會回到它所屬的 View 繼續處理。
這時 consumerMarkedDirty() 會進一步呼叫 markAncestorsForTraversal():
consumerMarkedDirty: (node: ReactiveLViewConsumer) => {
markAncestorsForTraversal(node.lView!);
}
這裡的 node.lView,就是這個 Consumer 所對應的 View。
Angular 接著會從這個 View 往上找到祖先,並在沿途留下標記。
假設目前的 View Tree 是:
AppComponent
└── LayoutComponent
└── ProductComponent
而真正受到影響的是 ProductComponent,Angular 就會往上處理 LayoutComponent 和 AppComponent。
這裡不是把所有祖先 View 都標記成「需要重新執行 Template」,而是告訴 Angular:這個分支下面還有 View 要處理,之後執行 Change Detection 時還要繼續往下。
概念上可以先理解成:
AppComponent
HasChildViewsToRefresh
↓
LayoutComponent
HasChildViewsToRefresh
↓
ProductComponent
Consumer dirty
補充:Angular 內部會透過
HasChildViewsToRefresh這個 flag 來記錄這件事情,代表這個 View 的子樹裡還有其他 View 需要處理,之後執行Change Detection時要繼續往下檢查。
至於目前這個 View 自己是否需要重新檢查,則要看它本身的狀態。HasChildViewsToRefresh 關心的是「下面還要不要繼續往下走」,而 Consumer 的 dirty 則表示「目前這個 View 自己是否可能需要進一步檢查」。
路徑留下來之後,下一次執行 Change Detection 時,Angular 就會根據這些標記沿著 View Tree 往下處理。
這時可以先看兩個狀態:
dirty:代表這個 View 本身可能需要重新檢查,Angular 會再確認它依賴的 Producer 是否真的發生變化。HasChildViewsToRefresh:代表這個 View 的下面還有其他 View 需要處理,所以 Change Detection 還要繼續往下走。以前面的例子來看,AppComponent 和 LayoutComponent 主要是保留往下走的路徑。
當 Angular 走到 ProductComponent 時,因為它的 Consumer 已經被標記為 dirty,就會進一步確認依賴的 Producer 是否真的有變化,再決定這個 View 要不要重新檢查。
而且 dirty 和 HasChildViewsToRefresh 並不是互斥的,同一個 View 也可能同時存在這兩種狀態。
例如 LayoutComponent 自己讀取的另一個 Signal 也發生變化時,就可能變成:
AppComponent
HasChildViewsToRefresh
↓
LayoutComponent
Consumer dirty
HasChildViewsToRefresh
↓
ProductComponent
Consumer dirty
這時 LayoutComponent 自己也可能需要重新檢查;同時因為它還帶有 HasChildViewsToRefresh,代表下面的 ProductComponent 也需要處理,所以 Change Detection 還是會繼續往下走。
當 Angular 確認某個 View 需要重新檢查後,就會重新執行其中的 Template binding。
例如:
<p>{{ count() }}</p>
當這個 View 被重新檢查時,Angular 會再次取得 count() 的結果,產生這一次的 binding value。
不過 Template 重新執行,並不代表 DOM 就一定會跟著修改。
Angular 會把每個 binding 的值保存在 LView 對應的位置。重新計算之後,再拿這次的結果和上一次保存的值進行比較。
簡化來看,底層的 bindingUpdated() 大致會做這件事情:
export function bindingUpdated(
lView: LView,
bindingIndex: number,
value: any,
): boolean {
const oldValue = lView[bindingIndex];
if (Object.is(oldValue, value)) {
return false;
}
lView[bindingIndex] = value;
return true;
}
整體流程可以大致理解如下:
所以從 Signal 發生變化到最後更新畫面,中間其實會經過好幾個階段。
Angular 會先透過 Reactive Graph 找到受到影響的 Consumer,再透過 View Tree 保留 Change Detection 需要經過的路徑。
當 Change Detection 走到真正需要重新檢查的 View 時,才會重新執行對應的 Template binding。
因為 Angular 在 Template 編譯階段,就已經知道有哪些 binding,以及它們各自對應的位置,所以可以直接重新計算對應的值,再和 LView 中保存的舊值進行比較。
只有當新舊 binding value 真的不同時,Angular 才會進一步更新對應的 DOM。
回到 Day 2 整理的簡化流程,今天其實把後半段一次走完了:
dirty 後,會透過 markAncestorsForTraversal() 替祖先 View 加上 HasChildViewsToRefresh,留下一條往下走的路徑。Change Detection:沿著 View Tree 往下走,帶有標記的分支會繼續往下,真正受到影響的 View 才會重新檢查。binding value 並更新 DOM:重新執行 Template binding 後,會將新的 binding value 和 LView 中保存的舊值進行比較,只有不同時才進一步更新 DOM。所以 Signal 帶來的不只是「值變了會通知」,它也讓 Angular 在執行 Change Detection 時知道該往哪裡走、哪些分支可以略過,進一步縮小需要檢查的範圍。
consumerMarkDirty() 設定 dirty、通知下游 Consumer,最後呼叫 consumerMarkedDirty()。ReactiveLViewConsumer,在 consumerMarkedDirty() 中呼叫 markAncestorsForTraversal()。markAncestorsForTraversal() 從目前的 View 往上,替祖先 View 加上 HasChildViewsToRefresh。LViewFlags 的定義,包含 HasChildViewsToRefresh。detectChangesInView() 依照 Consumer 的 dirty 與 HasChildViewsToRefresh,決定要重新檢查這個 View,還是只繼續往下走。bindingUpdated() 用 Object.is() 比較新舊 binding value,判斷這個 binding 是否發生變化。Producer、Consumer 與依賴追蹤的底層設計。